eletrotupi / tcc/ frontend/app/entry/[id].tsxceec19494f8065b22d45311703c93bbf20f546ca
13.0 KB | 431 lines Raw
1
import { useState, useEffect } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11

12
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
13
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
14
import { Badge } from "@/components/ui/Badge";
15
import { Button } from "@/components/ui/Button";
16
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
17
import { Section, SectionHeader } from "@/components/ui/Sections";
18
import { Theme, Colors, Spacing } from "@/constants/theme";
19
import { getMood } from "@/constants/moods";
20
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
21
import { formatMoment } from "@/lib/utils/time";
22
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
23
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
24
import { getTriggerCategory } from "@/constants/trigger-categories";
25
import { getCareActionMeta } from "@/constants/care-action-categories";
26

27
const impactColor = (n: number) => {
28
  if (n <= 1) return '#86efac';
29
  if (n <= 2) return '#22c55e';
30
  if (n <= 3) return '#f59e0b';
31
  if (n <= 4) return '#f97316';
32
  return '#ef4444';
33
};
34

35
export default function MoodDetailScreen() {
36
  const { id } = useLocalSearchParams<{ id: string }>();
37
  const { data: entry, isLoading, isError } = useMoodEntry(id);
38
  const deleteMoodEntry = useDeleteMoodEntry();
39
  const [minutesLeft, setMinutesLeft] = useState(0);
40
  const [canDelete, setCanDelete] = useState(false);
41

42
  useEffect(() => {
43
    const updateTimer = () => {
44
      if (!entry?.moment) return;
45
      const createdAt = new Date(entry.moment);
46
      const msSinceCreation = Date.now() - createdAt.getTime();
47
      const deletionWindowMs = 5 * 60 * 1000;
48

49
      const newCanDelete = msSinceCreation < deletionWindowMs;
50
      const newMinutesLeft = Math.max(
51
        0,
52
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
53
      );
54

55
      setCanDelete(newCanDelete);
56
      setMinutesLeft(newMinutesLeft);
57
    };
58

59
    updateTimer(); // Set initial values
60

61
    const interval = setInterval(updateTimer, 1000);
62

63
    return () => clearInterval(interval);
64
  }, [entry?.moment]);
65

66
  if (isLoading) {
67
    return (
68
      <View style={styles.centered}>
69
        <ActivityIndicator size="large" color={Theme.colors.light.tint} />
70
      </View>
71
    );
72
  }
73

74
  if (isError || !entry) {
75
    return (
76
      <View style={styles.centered}>
77
        <Text style={styles.errorText}>Registro não encontrado.</Text>
78
        <Pressable onPress={() => router.back()} style={styles.backButton}>
79
          <Text style={styles.backButtonText}>Voltar</Text>
80
        </Pressable>
81
      </View>
82
    );
83
  }
84

85
  const moment = new Date(entry.moment);
86
  const moodDef = getMood(entry.selectedMood.toLowerCase());
87

88
  const handleDelete = async () => {
89
    await deleteMoodEntry.mutateAsync(String(entry.id));
90

91
    router.back();
92
  };
93

94
  const deleteButtonMsg = () => {
95
    const msg = "Excluir Registro";
96
    const timeLeftStr = `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
97
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
98

99
    return `${msg} ${suffix}`;
100
  };
101

102
  const hasAnyLinks = (
103
    entry.triggerLinks && entry.triggerLinks.length > 0
104
  ) || (entry.careActions && entry.careActions.length > 0);
105

106
  return (
107
    <>
108
      <Stack.Screen options={{ title: "Ver detalhes" }} />
109

110
      <ScrollView
111
        style={styles.container}
112
        contentContainerStyle={styles.content}
113
        showsVerticalScrollIndicator={false}
114
      >
115
        {/* Header card */}
116
        <Card style={styles.headerCard}>
117
          <Between>
118
            <Row gap={12} style={{ alignItems: "center" }}>
119
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
120
              <Col gap={2}>
121
                <Text style={styles.moodLabel}>
122
                  {moodDef?.label ?? entry.selectedMood}
123
                </Text>
124
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
125
              </Col>
126
            </Row>
127
            {/* If this is the latest mood entry, show the active badge */}
128
            <Badge label="Registro Ativo" variant="green" />
129
          </Between>
130
        </Card>
131

132
        {/* Core levels */}
133
        <Section>
134
          <SectionHeader title="Níveis emocionais" variant="subtle" />
135
          {[
136
            { id: "energy", label: "Energia", value: entry.energyLevel },
137
            { id: "stress", label: "Estresse", value: entry.stressLevel },
138
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
139
          ].map((item) => (
140
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
141
              <View key={item.label}>
142
                <View style={styles.componentRow}>
143
                  <Text style={styles.componentLabel}>{item.label}</Text>
144
                  <Text style={styles.componentIntensityGreen}>
145
                    {item.value}/10
146
                  </Text>
147
                </View>
148

149
                <View style={styles.progressTrack}>
150
                  <View
151
                    style={[
152
                      styles.progressFill,
153
                      { width: `${(item.value / 10) * 100}%` },
154
                    ]}
155
                  />
156
                </View>
157
              </View>
158
            </Card>
159
          ))}
160
        </Section>
161

162
        <Section>
163
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
164
          <Col gap={0}>
165
            {entry.moodComponents.map((comp) => {
166
              return (
167
                <MoodComponentCard
168
                  key={comp.id}
169
                  id={comp.component.toLowerCase()}
170
                  intensity={comp.intensity}
171
                />
172
              );
173
            })}
174
          </Col>
175
        </Section>
176

177
        {/* Linked triggers */}
178
        <Section>
179
          <SectionHeader title="Vínculo" variant="subtle" />
180
          {!!hasAnyLinks ? (
181
            <Col gap={8}>
182
              {(entry.triggerLinks ?? []).map((link) => {
183
                const cat = getTriggerCategory(link.trigger.category);
184
                return (
185
                  <Pressable
186
                    key={link.id}
187
                    onPress={() => router.push(`/triggers/${link.trigger.id}`)}
188
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
189
                  >
190
                    <Card style={styles.linkCard}>
191
                      <Row style={{ alignItems: "center", gap: 12 }}>
192
                        <View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
193
                          <MaterialIcons name={cat.icon} size={22} color={cat.color} />
194
                        </View>
195
                        <Col gap={3} style={{ flex: 1 }}>
196
                          <Row style={{ alignItems: "center", gap: 6 }}>
197
                            <Text style={styles.linkLabel}>{cat.label}</Text>
198
                            <Badge label="Gatilho" variant="orange" style={undefined} />
199
                          </Row>
200
                          <Text style={styles.linkTime}>
201
                            {formatMoment(new Date(link.trigger.moment))}
202
                          </Text>
203
                        </Col>
204
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
205
                      </Row>
206
                      <View style={styles.impactBar}>
207
                        <View
208
                          style={[
209
                            styles.impactFill,
210
                            {
211
                              width: `${(link.perceivedImpact / 5) * 100}%`,
212
                              backgroundColor: impactColor(link.perceivedImpact),
213
                            },
214
                          ]}
215
                        />
216
                      </View>
217
                    </Card>
218
                  </Pressable>
219
                );
220
              })}
221

222
              {(entry.careActions ?? []).map((ca) => {
223
                const meta = getCareActionMeta(ca);
224

225
                return (
226
                  <Pressable
227
                    key={ca.id}
228
                    onPress={() => router.push(`/care-actions/${ca.id}`)}
229
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
230
                  >
231
                    <Card style={styles.linkCard}>
232
                      <Row style={{ alignItems: 'center', gap: 12 }}>
233
                        <View style={[styles.linkIconCircle, { backgroundColor: meta.iconBackground }]}>
234
                          <MaterialIcons name={meta.icon} size={22} color={meta.color} />
235
                        </View>
236
                        <Col gap={3} style={{ flex: 1 }}>
237
                          <Row style={{ alignItems: 'center', gap: 6 }}>
238
                            <Text style={styles.linkLabel}>{meta.label}</Text>
239
                            <Badge label="Consulta" variant="blue" style={undefined} />
240
                          </Row>
241
                          <Text style={styles.linkTime}>
242
                            {formatMoment(new Date(ca.moment))}
243
                          </Text>
244
                        </Col>
245

246
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
247
                      </Row>
248
                    </Card>
249
                  </Pressable>
250
                );
251
              })}
252
            </Col>
253
          ) : (
254
            <NoLinkMessage />
255
          )}
256
        </Section>
257

258
        {/* Annotation / notes */}
259
        {entry.annotation && (
260
          <Section>
261
            <SectionHeader title="Anotações" variant="subtle" />
262
            <Card style={{ padding: Spacing.cardGap }}>
263
              <Text style={styles.annotation}>
264
                &quot;{entry.annotation?.trim()}&quot;
265
              </Text>
266
            </Card>
267
          </Section>
268
        )}
269

270
        {/* Delete */}
271
        <View style={styles.section}>
272
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
273
          <Button
274
            variant="danger"
275
            loading={deleteMoodEntry.isPending}
276
            disabled={!canDelete || deleteMoodEntry.isPending}
277
            onPress={handleDelete}
278
            title={deleteButtonMsg()}
279
          />
280

281
          <SubtleInfoCard
282
            style={styles.deleteInfo}
283
            text={`
284
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
285
              após o envio para garantir a integridade do histórico emocional.
286
              `}
287
          />
288
        </View>
289
      </ScrollView>
290
    </>
291
  );
292
}
293

294
const styles = StyleSheet.create({
295
  container: {
296
    flex: 1,
297
    backgroundColor: Theme.colors.light.background,
298
  },
299
  content: {
300
    padding: Spacing.containerPadding,
301
    gap: Spacing.sectionGap,
302
    paddingBottom: 48,
303
  },
304
  centered: {
305
    flex: 1,
306
    justifyContent: "center",
307
    alignItems: "center",
308
    gap: 12,
309
  },
310
  errorText: {
311
    fontSize: 15,
312
    color: Colors.light.textSecondary,
313
  },
314
  backButton: {
315
    paddingHorizontal: 20,
316
    paddingVertical: 10,
317
    backgroundColor: Theme.colors.light.tint,
318
    borderRadius: Theme.borderRadius.md,
319
  },
320
  backButtonText: {
321
    fontWeight: "700",
322
    color: Theme.colors.light.text,
323
  },
324

325
  // Header
326
  headerCard: {
327
    padding: 16,
328
  },
329
  moodIcon: {
330
    fontSize: 40,
331
  },
332
  moodLabel: {
333
    fontSize: 17,
334
    fontWeight: "700",
335
    color: Colors.light.text,
336
  },
337
  moodTime: {
338
    fontSize: 13,
339
    color: Colors.light.textSecondary,
340
  },
341

342
  // Sections
343
  section: {
344
    gap: 8,
345
  },
346
  sectionLabel: {
347
    fontSize: 11,
348
    fontWeight: "600",
349
    letterSpacing: 0.8,
350
    color: Colors.light.textSecondary,
351
    textTransform: "uppercase",
352
  },
353

354
  // Components
355
  componentRow: {
356
    flexDirection: "row",
357
    justifyContent: "space-between",
358
    alignItems: "center",
359
    paddingVertical: 10,
360
  },
361
  componentLabel: {
362
    fontSize: 15,
363
    fontWeight: "700",
364
    color: Colors.light.text,
365
  },
366
  componentIntensity: {
367
    fontSize: 13,
368
    color: Colors.light.textSecondary,
369
  },
370
  componentIntensityGreen: {
371
    fontSize: 13,
372
    fontWeight: "700",
373
    color: Theme.colors.light.tint,
374
  },
375
  progressTrack: {
376
    height: 6,
377
    backgroundColor: Theme.colors.light.divider,
378
    borderRadius: Theme.borderRadius.full,
379
    marginBottom: 4,
380
    overflow: "hidden",
381
  },
382
  progressFill: {
383
    height: "100%",
384
    backgroundColor: Theme.colors.light.tint,
385
    borderRadius: Theme.borderRadius.full,
386
  },
387
  // Annotation
388
  annotation: {
389
    fontSize: 14,
390
    lineHeight: 22,
391
    color: Colors.light.textSecondary,
392
    fontStyle: "italic",
393
    padding: 4,
394
  },
395
  deleteInfo: {
396
    marginTop: 4,
397
  },
398
  // Linked trigger cards
399
  linkCard: {
400
    padding: Spacing.cardGap,
401
    overflow: "hidden",
402
    gap: 10,
403
  },
404
  linkIconCircle: {
405
    width: 44,
406
    height: 44,
407
    borderRadius: 22,
408
    alignItems: "center",
409
    justifyContent: "center",
410
  },
411
  linkTime: {
412
    fontSize: 12,
413
    color: Colors.light.textSecondary,
414
  },
415
  linkLabel: {
416
    fontSize: 15,
417
    fontWeight: "700",
418
    color: Colors.light.text,
419
  },
420
  impactBar: {
421
    height: 3,
422
    backgroundColor: Colors.light.divider,
423
    borderRadius: 99,
424
    overflow: "hidden",
425
  },
426
  impactFill: {
427
    height: "100%",
428
    borderRadius: 99,
429
  },
430

431
});